package observations

import (
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/kelseyhightower/envconfig"

	ma "github.com/multiformats/go-multiaddr"

	"github.com/ipfs/ipfs-cluster/config"
)

const metricsConfigKey = "metrics"
const tracingConfigKey = "tracing"
const metricsEnvConfigKey = "cluster_metrics"
const tracingEnvConfigKey = "cluster_tracing"

// Default values for this Config.
const (
	DefaultEnableStats        = false
	DefaultPrometheusEndpoint = "/ip4/127.0.0.1/tcp/8888"
	DefaultReportingInterval  = 2 * time.Second

	DefaultEnableTracing       = false
	DefaultJaegerAgentEndpoint = "/ip4/0.0.0.0/udp/6831"
	DefaultSamplingProb        = 0.3
	DefaultServiceName         = "cluster-daemon"
)

// MetricsConfig configures metrics collection.
type MetricsConfig struct {
	config.Saver

	EnableStats        bool
	PrometheusEndpoint ma.Multiaddr
	ReportingInterval  time.Duration
}

type jsonMetricsConfig struct {
	EnableStats        bool   `json:"enable_stats"`
	PrometheusEndpoint string `json:"prometheus_endpoint"`
	ReportingInterval  string `json:"reporting_interval"`
}

// ConfigKey provides a human-friendly identifier for this type of Config.
func (cfg *MetricsConfig) ConfigKey() string {
	return metricsConfigKey
}

// Default sets the fields of this Config to sensible values.
func (cfg *MetricsConfig) Default() error {
	cfg.EnableStats = DefaultEnableStats
	endpointAddr, _ := ma.NewMultiaddr(DefaultPrometheusEndpoint)
	cfg.PrometheusEndpoint = endpointAddr
	cfg.ReportingInterval = DefaultReportingInterval

	return nil
}

// ApplyEnvVars fills in any Config fields found
// as environment variables.
func (cfg *MetricsConfig) ApplyEnvVars() error {
	jcfg := cfg.toJSONConfig()

	err := envconfig.Process(metricsEnvConfigKey, jcfg)
	if err != nil {
		return err
	}

	return cfg.applyJSONConfig(jcfg)
}

// Validate checks that the fields of this Config have working values,
// at least in appearance.
func (cfg *MetricsConfig) Validate() error {
	if cfg.EnableStats {
		if cfg.PrometheusEndpoint == nil {
			return errors.New("metrics.prometheus_endpoint is undefined")
		}
		if cfg.ReportingInterval < 0 {
			return errors.New("metrics.reporting_interval is invalid")
		}
	}
	return nil
}

// LoadJSON sets the fields of this Config to the values defined by the JSON
// representation of it, as generated by ToJSON.
func (cfg *MetricsConfig) LoadJSON(raw []byte) error {
	jcfg := &jsonMetricsConfig{}
	err := json.Unmarshal(raw, jcfg)
	if err != nil {
		logger.Error("Error unmarshaling observations config")
		return err
	}

	cfg.Default()

	return cfg.applyJSONConfig(jcfg)
}

func (cfg *MetricsConfig) applyJSONConfig(jcfg *jsonMetricsConfig) error {
	err := cfg.loadMetricsOptions(jcfg)
	if err != nil {
		return err
	}

	return cfg.Validate()
}

func (cfg *MetricsConfig) loadMetricsOptions(jcfg *jsonMetricsConfig) error {
	cfg.EnableStats = jcfg.EnableStats
	endpointAddr, err := ma.NewMultiaddr(jcfg.PrometheusEndpoint)
	if err != nil {
		return fmt.Errorf("loadMetricsOptions: PrometheusEndpoint multiaddr: %v", err)
	}
	cfg.PrometheusEndpoint = endpointAddr

	return config.ParseDurations(
		metricsConfigKey,
		&config.DurationOpt{
			Duration: jcfg.ReportingInterval,
			Dst:      &cfg.ReportingInterval,
			Name:     "metrics.reporting_interval",
		},
	)
}

// ToJSON generates a human-friendly JSON representation of this Config.
func (cfg *MetricsConfig) ToJSON() ([]byte, error) {
	jcfg := cfg.toJSONConfig()

	return config.DefaultJSONMarshal(jcfg)
}

func (cfg *MetricsConfig) toJSONConfig() *jsonMetricsConfig {
	return &jsonMetricsConfig{
		EnableStats:        cfg.EnableStats,
		PrometheusEndpoint: cfg.PrometheusEndpoint.String(),
		ReportingInterval:  cfg.ReportingInterval.String(),
	}
}

// ToDisplayJSON returns JSON config as a string.
func (cfg *MetricsConfig) ToDisplayJSON() ([]byte, error) {
	return config.DisplayJSON(cfg.toJSONConfig())
}

// TracingConfig configures tracing.
type TracingConfig struct {
	config.Saver

	EnableTracing       bool
	JaegerAgentEndpoint ma.Multiaddr
	SamplingProb        float64
	ServiceName         string
	ClusterID           string
	ClusterPeername     string
}

type jsonTracingConfig struct {
	EnableTracing       bool    `json:"enable_tracing"`
	JaegerAgentEndpoint string  `json:"jaeger_agent_endpoint"`
	SamplingProb        float64 `json:"sampling_prob"`
	ServiceName         string  `json:"service_name"`
}

// ConfigKey provides a human-friendly identifier for this type of Config.
func (cfg *TracingConfig) ConfigKey() string {
	return tracingConfigKey
}

// Default sets the fields of this Config to sensible values.
func (cfg *TracingConfig) Default() error {
	cfg.EnableTracing = DefaultEnableTracing
	agentAddr, _ := ma.NewMultiaddr(DefaultJaegerAgentEndpoint)
	cfg.JaegerAgentEndpoint = agentAddr
	cfg.SamplingProb = DefaultSamplingProb
	cfg.ServiceName = DefaultServiceName
	return nil
}

// ApplyEnvVars fills in any Config fields found
// as environment variables.
func (cfg *TracingConfig) ApplyEnvVars() error {
	jcfg := cfg.toJSONConfig()

	err := envconfig.Process(tracingEnvConfigKey, jcfg)
	if err != nil {
		return err
	}

	return cfg.applyJSONConfig(jcfg)
}

// Validate checks that the fields of this Config have working values,
// at least in appearance.
func (cfg *TracingConfig) Validate() error {
	if cfg.EnableTracing {
		if cfg.JaegerAgentEndpoint == nil {
			return errors.New("tracing.jaeger_agent_endpoint is undefined")
		}
		if cfg.SamplingProb < 0 {
			return errors.New("tracing.sampling_prob is invalid")
		}
	}
	return nil
}

// LoadJSON sets the fields of this Config to the values defined by the JSON
// representation of it, as generated by ToJSON.
func (cfg *TracingConfig) LoadJSON(raw []byte) error {
	jcfg := &jsonTracingConfig{}
	err := json.Unmarshal(raw, jcfg)
	if err != nil {
		logger.Error("Error unmarshaling observations config")
		return err
	}

	cfg.Default()

	return cfg.applyJSONConfig(jcfg)
}

func (cfg *TracingConfig) applyJSONConfig(jcfg *jsonTracingConfig) error {
	err := cfg.loadTracingOptions(jcfg)
	if err != nil {
		return err
	}

	return cfg.Validate()
}

func (cfg *TracingConfig) loadTracingOptions(jcfg *jsonTracingConfig) error {
	cfg.EnableTracing = jcfg.EnableTracing
	agentAddr, err := ma.NewMultiaddr(jcfg.JaegerAgentEndpoint)
	if err != nil {
		return fmt.Errorf("loadTracingOptions: JaegerAgentEndpoint multiaddr: %v", err)
	}
	cfg.JaegerAgentEndpoint = agentAddr
	cfg.SamplingProb = jcfg.SamplingProb
	cfg.ServiceName = jcfg.ServiceName

	return nil
}

// ToJSON generates a human-friendly JSON representation of this Config.
func (cfg *TracingConfig) ToJSON() ([]byte, error) {
	jcfg := cfg.toJSONConfig()

	return config.DefaultJSONMarshal(jcfg)
}

func (cfg *TracingConfig) toJSONConfig() *jsonTracingConfig {
	return &jsonTracingConfig{
		EnableTracing:       cfg.EnableTracing,
		JaegerAgentEndpoint: cfg.JaegerAgentEndpoint.String(),
		SamplingProb:        cfg.SamplingProb,
		ServiceName:         cfg.ServiceName,
	}
}

// ToDisplayJSON returns JSON config as a string.
func (cfg *TracingConfig) ToDisplayJSON() ([]byte, error) {
	return config.DisplayJSON(cfg.toJSONConfig())
}
